---
title: 数据库设计
description: 怎样按照领域模型设计数据库？
---

模型驱动设计可以分为两部分：模型的建立和模型的实现。模型的建立要求模型和业务需求一致，模型的实现要求和模型一致。

现在，已经建立了领域模型，并且从理论层面对模型驱动设计的概念进行了总结，这些都属于模型的建立。而现在要做的数据库设计，则属于模型的实现。

那么，怎样由领域模型，一步一步地推导出数据库的设计呢？这种方法和以前的方法有什么不同呢？这节课我们就来讨论这两个问题。在这个过程中，我们要着重体会 <RedSpan>数据库设计是如何与领域模型保持一致的</RedSpan>。



数据库设计在软件工程中叫做建立 <RedSpan>物理数据模型（physical data model, PDM）</RedSpan>，主要目的就是对数据表进行设计。具体来说，包括以下几点：
* 建立那些表；
* 表中有那些字段；
* 表的主键和外键是什么；
* 字段的数据类型以及约束；

我们在领域建模的时候把模型分成了四个模块，下面，我们就一个模块一个模块地进行数据库设计。

<img src={'https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241201195150.png'}/>

## 租户管理
下面是租户管理模块的模型图。
<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241201223144.png"/>

一般来说，<RedSpan>一个实体可以映射为一个数据库表</RedSpan>。所以，先根据租户实体设计出租户表。可以用下面的符号表示。


<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241201223845.png"/>

和 UML 不同，<RedSpan>物理数据模型的图示法并没有统一的国际标准</RedSpan>。所以不同的专家、不同的工具，画出来的都不太一样。这里我用了绘图工具（draw.io）中提供的符号。


另外，还可以用建表语句（create table） 表示表的结构。建表语句和图形符号是同一个意思的两种等价的表示方法，比如上图可以直接翻译成下面的建表语句：

```sql
CREATE TABLE tenant (
    id INT PRIMARY KEY
);
```
所以，原则上直接用建表语句进行数据库设计也是可以的。

首先看这个符号的第一行，这里的 tenant 是表的名称。在领域建模阶段，为了和领域专家进行沟通，模型中使用的都是中文。但建表时，一般要用英文来命名。那问题就来了，怎么保证中英文的一致，从而在实现层面贯彻统一语言呢？

答案就是使用我们前面建的词汇表。在词汇表里，我们规定了每个中文词汇对应的英文全称和简称。在为数据库表以及字段等命名时，如果词汇表中有简称就用简称，否则就用全称。我们把词汇表在这里再列一遍作为参考。
<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241201204723.png"/>

词汇表里，查到租户的英文是 tenant，所以用它作为表的名称。

<RedSpan>然后我们再看这个表符号的第二行，这是表的主键</RedSpan>，包括主键的名称 “id” 和数据类型 “int” 。“PK”（primary key）表示这个字段是一个主键。

除了这种命名法以外，主键命名的另一种常见做法是 <RedSpan>包含表名</RedSpan>，也就是命名为 tenant_id。两种方法都可以，根据你具体项目的规定来选择就行了。

**最后，我们再为这个表添加其他字段。领域模型中的属性，一般会映射成表中的字段。**

在领域建模的时候，为了模型的简洁和稳定，我们主张只写出有助于表达实体含义的主要属性，一些不言自明的属性就不用写了。但在数据库设计阶段，就要根据需求列出所有字段了。这里，我们要补充下面几个字段：

<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241201225847.png"/>

name 表示租户的名称。created_at，created_by，last_updated_at 和 last_updated_by 分别表示一条记录的创建时间、创建人、最后一次修改时间和最后一次修改人。创建人和最后修改人保存的是用户的 id。

这四个字段常常被称为 <RedSpan>审计字段</RedSpan>，可以用来进行安全审计和错误排查。作为一种最佳实践，我们在每一个表中都会包含这四个字段。

## 组织管理
组织管理的领域模型如下：
<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241201230520.png"/>

首先，我们用类似的方法为 <RedSpan>组织实体</RedSpan>建表，如下图：

<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241201230829.png"/>

先看图中的<RedSpan>实线箭头</RedSpan>，这个箭头代表外键参照关系。按照领域模型，租户和组织是一对多关联。<RedSpan>一个一对多关联，在数据库设计时可以映射成一个外键</RedSpan>。


图中的 FK（foreign key) 代表外键。FK = tenant_id 说明 org 表中指向 tenant 表的外键是 tenant_id 字段。

<RedSpan>此外，我们还添加了非空（NOT NULL）约束。</RedSpan>这和领域模型中的多重性有关。租户和组织间的关联，在租户端实际是“1..1”，也就是说一个组织至少会关联一个租户，最多也只能关联一个租户。“1..1”前面的“1”就映射成了组织表里 tenant_id 字段后面的非空约束。假如不是“1..1”而是“0..1”，那么就不会有 NOT NULL 了。所以，关联上的多重性决定了外键字段的非空约束。

不过，在基于云的应用里，为了减少数据库处理的瓶颈，一般不主张建立真正的外键，而是用程序来保证外键约束。但是在物理数据模型里，我们又希望表达外键参照，方便理解数据表间的关系。这时候，我们可以把实线箭头换成虚线箭头，表示 <RedSpan>虚拟外键</RedSpan>，如下图：

<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241201231201.png"/>

后面我们所有的数据表设计都采用虚拟外键。

事实上，数据库中其他所有表都有一个指向 tenant 表的虚拟外键，以便区分是哪个租户的数据。如果每个虚拟外键都画出来，我们的图会变得很乱，所以后面就只在表中写出 tenant_id，不画箭头了，我们可以用一个注释说明这件事。

<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241201231236.png"/>

接下来，咱们用类似的方法完成 <RedSpan>组织、组织类别和员工实体</RedSpan>。如下图：

<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241201231321.png"/>

我们为这张图补充几点说明。

首先看 **org（组织）**表，这里有一个指向自身的虚拟外键 superior_id ，表示组织之间的上级关系，对应于领域模型中的自关联。

然后还可以看到 **emp（员工）**表和 **org** 表之间有两个方向相反的虚拟外键，一个表示**组织的负责人**关系，另一个表示**员工归属于哪个组织**。

另外，**emp** 表中的 num、id_num、name、gender、dob 分别表示员工号、身份证号、姓名、性别和出生日期（date of birth）。

最后，我们来处理岗位。

<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241201231916.png"/>

岗位和员工之间是 <RedSpan>多对多关联。这时，我们必须增加一个关联表，来表达两者之间的关系</RedSpan>，如下图：

<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241201232028.png"/>

这里，我们增加了 <RedSpan>emp_post 表来表达多对多的关联</RedSpan>。表中包含了 post (岗位) 和 emp 两个表的主键作为自己的虚拟外键。我们采用了由 emp_id 和 post_id 两个字段组成的复合主键，因为这时添加一个单独的 id 主键并没有什么意义。

一般来说，我们都主张用单独的 id 主键，只有符合以下两个条件时，才应该使用上面这种联合主键：
1. <RedSpan>两个外键字段</RedSpan>，例如 emp_id 和 post_id ，唯一决定了一条记录；
2. <RedSpan>这个表的主键没有被其他表作为外键引用</RedSpan>。


## 项目管理
下面是项目管理模块的领域模型图，省略了和项目管理无关的部分。

<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241201232314.png"/>

用前面的方法，我们可以画出项目管理模块的物理数据模型图：

<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241201233153.png"/>

在这张图里，有没有注意到员工表的名称写成了 emp: 2 ，而且除了主键以外没有其他属性？

这其实是一种绘图技巧。为了避免整张图像蜘蛛网一样凌乱，我们不打算把所有表都画在同一张图上，而是每个模块画一张。而 emp 表在组织管理中出现过一次，在项目管理中又出现了，所以我们用了 emp: 2 说明这是 emp 表的第 “2” 次出现。

至于属性，我们只在 emp 表第一次出现的时候详细写出来就可以了，其他地方不写属性，这样，当需要更改属性的时候，只改一个地方就可以了。

## 工时管理
工时管理模块的领域模型图如下：
<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241201233254.png"/>


用我们前面的知识，很容易就能做出下面的设计：
<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241201233533.png"/>
到这里，数据库设计就完成了。

## 和传统数据库设计的对比
要回答这个问题，我们先来明确一下这里所谓以前的方法指什么。传统的软件工程中本来就有一套以 ER 图为工具、规范的数据库设计方法。不过我们多数小伙伴并没有严格按照这种方法去做，而是直接拍脑袋设计数据表。我们可以把这两种方法称为“ER 图法”和“拍脑袋法”。

### 与“拍脑袋法”的区别

如果我们只是靠直觉设计数据库，不去深入分析领域知识，虽然刚开始时可能可以满足业务需求，但随着需求越来越复杂，问题就会逐渐浮现出来。

首先我们要知道，无论是数据模型图还是建表语句，都是面向技术人员的，业务专家很难理解。所以我们无法使用这些方式和业务专家沟通，也就很难保证数据库设计能准确地反映领域知识。而按照 DDD 的方法，我们可以先基于领域模型和业务专家对齐需求，再把领域模型转换为数据库设计，从而解决领域知识的沟通问题。

第二个问题在于，这样随意的数据库设计，很可能会违反数据库设计的范式，造成数据冗余和潜在错误。范式（NF）是规范形式（Normal Form）的简称，核心思想在于避免数据的冗余。也就是说，数据表的范式越高，数据冗余就越少。


不过在实践中，一般做到第三范式就够了。范式并不是用于直接进行数据库设计的，而是正确的数据库设计的反向验证。正确地运用领域模型进行数据库设计，一般而言，就不会违反第三范式了。

### 与 E-R 图法的区别

传统的软件工程，是按照“概念设计”“逻辑设计”和“物理设计”的步骤进行数据库设计的。其中概念设计和逻辑设计，通常会采用 ER 图，也就是实体联系图。ER 图同样没有业界统一的标准，有多种画法。下面这张图用了一种常用的画法，表示组织和员工的一对多关系。

<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241201233819.png"/>

这里要注意一点，有些人以为前面的物理数据模型图就是 ER 图，其实是不对的。ER 图的关注点和领域模型图类似，是实体以及实体之间的关联关系，而物理数据模型图关注的是表、字段、主键和外键等等。

首先，采用 UML 类图描述的领域模型图是 ER 图的超集。也就是说，ER 图能表达的，领域模型图都能表达；而领域模型图能表达的，ER 图未必能表达。因此，使用领域模型图以后，我们就不必再使用 ER 图了。

其实我们前几节课进行的领域建模，大体上相当于传统意义上的“概念设计”。如果把领域模型中的属性都补全，就相当于传统意义的“逻辑设计”了。而我们今天做的，其实就是传统上的“物理设计”，所以产物叫做“物理数据模型”。

第二个区别是，ER 图只能表达静态的数据关系，只用于数据库设计，而领域模型图则可以将静态数据和动态行为绑定，不仅可以用于数据库设计，还可以用于程序设计，这一点我们在后面的课程会看到。也就是说，基于 DDD 的方法能够保证程序设计和数据库设计的高度统一。

第三个区别是，领域模型对应的主要是传统软件工程的分析模型，而 ER 图在传统软件工程里则处于设计阶段，所以两者的层次和使用场合也是不一样的。

## 总结

DDD 主张要根据领域模型来进行数据库设计，保证数据库和领域模型的一致，从而保证数据库和业务需求以及代码的一致性。在进行数据库设计时，我们可以用物理数据模型图，也可以直接用建表语句，两者基本是等价的。图示的方法更加直观。

对数据表、字段等等的命名，应该依据词汇表，以便保证统一语言。一般来说，领域模型中的实体映射为数据库中的表；领域模型中的属性，映射成表中的字段。同时还要根据需求补充更多的字段。

模型中的一个一对多关联，可以映射成一个外键字段，以及一个外键约束。但基于云的应用一般不会真的建立外键约束，而外键的逻辑关系还是存在的。我们用虚线箭头表示这种逻辑上的外键关系，称为虚拟外键。对于多对多关联，我们必须增加一个关联表，其中包括了两个实体表各自的主键。另外，关联上的多重性决定了外键字段的非空约束。

基于 DDD 的数据库设计和以前方法的区别。比起“拍脑袋”的方法，DDD 的方法更容易和业务专家对齐领域知识，而且不容易违反数据库设计范式。另一方面，DDD 方法是 ER 图法的“超集”，并且能够将静态数据和动态逻辑整合在一起，达到业务、数据库和代码三者的统一。

:::tip 在多数数据表设计中都用了没有业务含义的 id 作为主键，这种做法比起使用有业务含义的字段做主键有什么优点？
首先由业务含义的字段虽然业务上不允许重复，但有些时候会出现意料之外的场景，比如拿学生的名字作为主键就会遇到重名的问题。<br/>
其次业务主键一般都为字符型，考虑到数据库优化，有的时候递增主键会带来一些效率。<br/>
再次业务上的唯一主键有些时候为联合主键，维护起来难度更加提升。<br/>
再再次，解耦业务需求和技术实现。 <br/>

还有就是用没有业务含义的id作为主键是一种分离关注点的设计方式；业务是在变化的，今天适合用来做主键的业务字段，未来未必，索性约定俗成用id算了
:::